package bullgo

import (
	"bullgo/readcommand"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/go-redis/redis/v8"
)

func isJobInList(exRds *readcommand.ExtendRedis, listKey string, jobID string) bool {
	Cmd := exRds.Run("isJobInList", []string{listKey}, jobID)
	if val := Cmd.Val(); val != nil && val.(int64) == 1 {
		return true
	}
	return false
}

func addJob(job Job, opts AddJobOpt) (*string, error) {
	queue := job.Queue
	queueKeys := queue.queueKeys
	dataByte, err := json.Marshal(job.Data)
	if err != nil {
		return nil, err
	}
	optsByte, err := json.Marshal(opts)
	if err != nil {
		return nil, err
	}
	keys := []string{
		queueKeys.Wait,
		queueKeys.Paused,
		queueKeys.MetaPaused,
		queueKeys.ID,
		queueKeys.Delayed,
		queueKeys.Priority,
	}
	args := []interface{}{
		queueKeys.Empty,
		opts.CustomJobID,
		job.Name,
		string(dataByte),
		string(optsByte),
		job.Timestamp,
		job.Delay,
		0,
		opts.Priority,
		"LPUSH",
		queue.token,
	}
	if opts.Lifo {
		args[9] = "RPUSH"
	}
	if job.Delay != 0 {
		args[7] = job.Delay + job.Timestamp
	}
	Cmd := queue.Run("addJob", keys, args...)
	val, err := Cmd.Result()
	if err != nil {
		return nil, err
	}
	jobID, _ := val.(string)
	return &jobID, nil
}

func pause(queue *Queue, pause bool) *redis.Cmd {
	src := "wait"
	dst := "paused"
	if !pause {
		src = "paused"
		dst = "wait"
	}
	keys := []string{}
	event := func() string {
		if pause {
			return "paused"
		}
		return "resumed"
	}()
	for _, val := range []string{src, dst, "metaPaused", event} {
		keys = append(keys, queue.Tokey(val))
	}
	return queue.Run("pause", keys, event)
}

func moveToActive(queue *Queue, jobID string) *redis.Cmd {
	queueKeys := queue.queueKeys
	keys := []string{queueKeys.Wait, queueKeys.Active, queueKeys.Priority}
	keys = append(keys, keys[1]+"@"+queue.token, queueKeys.Stalled, queueKeys.Limiter, queueKeys.Delayed, queueKeys.Drained)
	args := []interface{}{
		queueKeys.Empty,
		queue.token,
		queue.queueSettings.LockDuration,
		time.Now().UnixNano() / 1e6,
		jobID,
	}
	if queue.queueLimiter != nil {
		args = append(args, queue.queueLimiter.max, queue.queueLimiter.duration)
	}
	return queue.Run("moveToActive", keys, args...)
}

func updateProgress(job Job, progress int) *redis.Cmd {
	queue := job.Queue
	keys := []string{queue.Tokey(job.ID), queue.Tokey("progress")}
	event := job.ID + "," + strconv.Itoa(progress)
	return queue.Run("updateProgress", keys, progress, event)
}

func moveToFinishedArgs(
	job Job,
	val string,
	propVal string,
	shouldRemove bool,
	target string,
	ignoreLock bool,
	notFetch bool,
) ([]string, []interface{}) {
	queue := job.Queue
	queueKeys := queue.queueKeys

	keys := []string{
		queueKeys.Active,
		"",
		queue.Tokey(job.ID),
		queueKeys.Wait,
		queueKeys.Priority,
		queueKeys.Active + "@" + queue.token,
	}
	if target == Completed {
		keys[1] = queueKeys.Completed
	}
	if target == Failed {
		keys[1] = queueKeys.Failed
	}

	args := []interface{}{
		job.ID,
		time.Now().UnixNano() / 1e6,
		propVal,
		"",
		queue.token,
		"0",
		"",
		"1",
		queueKeys.Empty,
		queue.queueSettings.LockDuration,
		queue.token,
	}
	m := map[string]interface{}{"jobId": job.ID, "val": val}
	mjson, _ := json.Marshal(m)
	mString := string(mjson)
	if val != "" {
		args[3] = val
	}
	if shouldRemove {
		args[5] = true
	}
	if notFetch {
		args[7] = "0"
	}
	args[6] = mString

	return keys, args
}

func moveToFinished(
	job Job,
	val string,
	propVal string,
	shouldRemove bool,
	target string,
	ignoreLock bool,
	notFetch bool,
) error {
	keys, args := moveToFinishedArgs(
		job,
		val,
		propVal,
		shouldRemove,
		target,
		ignoreLock,
		notFetch,
	)
	Cmd := job.Queue.Run("moveToFinished", keys, args...)
	num := Cmd.Val().(int64)
	if num < 0 {
		return finishedErrors(num, job.ID, "finished")
	}
	return nil
}

func finishedErrors(code int64, jobID string, command string) error {
	switch code {
	case -1:
		return errors.New("Missing key for job " + jobID + " " + command)
	case -2:
		return errors.New("Missing lock for job " + jobID + " " + command)
	}
	return errors.New("unknown error")
}

func moveToCompleted(job Job, returnvalue string, removeOnComplete bool, ignoreLock bool) error {
	return moveToFinished(
		job,
		returnvalue,
		"returnvalue",
		removeOnComplete,
		"completed",
		ignoreLock,
		false,
	)
}

func moveToFailed(job Job, failedReason string, removeOnFailed bool, ignoreLock bool) error {
	return moveToFinished(
		job,
		failedReason,
		"failedReason",
		removeOnFailed,
		"failed",
		ignoreLock,
		true,
	)
}

func IsFinished(job Job) *redis.Cmd {
	return job.Queue.Run(
		"isFinished",
		[]string{
			job.Queue.queueKeys.Completed,
			job.Queue.queueKeys.Failed,
		},
		job.ID,
	)
}

func moveToDelayedArgs(queue *Queue, jobID string, timestamp int64, ignoreLock bool) ([]string, []interface{}) {
	jobIDnum, _ := strconv.ParseInt(jobID, 10, 64)
	timestamp = timestamp*0x1000 + (jobIDnum & 0xfff)
	keys := []string{queue.queueKeys.Active, queue.queueKeys.Delayed, queue.Tokey(jobID)}
	args := []interface{}{fmt.Sprint(timestamp), jobID, queue.token}
	if ignoreLock {
		args[2] = "0"
	}
	return keys, args
}

func moveToDelayed(queue *Queue, jobID string, timestamp int64, ignoreLock bool) error {
	keys, args := moveToDelayedArgs(queue, jobID, timestamp, ignoreLock)
	cmd := queue.Run("moveToDelayed", keys, args...)
	switch cmd.Val() {
	case -1:
		return errors.New("Missing Job " +
			jobID +
			" when trying to move from active to delayed")
	case -2:
		return errors.New("Job " +
			jobID +
			" was locked when trying to move from active to delayed")
	}
	return nil
}

func remove(queue *Queue, jobID string) *redis.Cmd {
	keys := []string{
		queue.queueKeys.Active,
		queue.queueKeys.Wait,
		queue.queueKeys.Delayed,
		queue.queueKeys.Paused,
		queue.queueKeys.Completed,
		queue.queueKeys.Failed,
		queue.queueKeys.Priority,
		queue.Tokey(jobID),
	}
	return queue.Run("removeJob", keys, jobID, queue.token)
}
func extendLock(queue *Queue, jobID string) *redis.Cmd {
	return queue.Run(
		"extendLock",
		[]string{
			queue.Tokey(jobID) + ":lock",
			queue.queueKeys.Stalled,
		},
		queue.token,
		queue.queueSettings.LockDuration,
		jobID,
	)
}

func releaseLock(queue *Queue, jobID string) *redis.Cmd {
	return queue.Run(
		"releaseLock",
		[]string{
			queue.Tokey(jobID) + ":lock",
		},
		queue.token,
	)
}

func takeLock(queue *Queue, jobID string) *redis.Cmd {
	return queue.Run(
		"takeLock",
		[]string{
			queue.Tokey(jobID) + ":lock",
		},
		queue.token,
		queue.queueSettings.LockDuration,
	)
}

func updateDelaySet(queue *Queue, delayedTimestamp int64) *redis.Cmd {
	keys := []string{
		queue.queueKeys.Delayed,
		queue.queueKeys.Active,
		queue.queueKeys.Wait,
		queue.queueKeys.Priority,
	}
	args := []interface{}{
		queue.queueKeys.Empty,
		delayedTimestamp,
		queue.token,
	}
	return queue.Run("updateDelaySet", keys, args...)
}

func moveUnlockedJobsToWait(queue *Queue) *redis.Cmd {
	keys := []string{
		queue.queueKeys.Stalled,
		queue.queueKeys.Wait,
		queue.queueKeys.Active,
		queue.queueKeys.Failed,
		queue.queueKeys.StalledCheck,
		queue.queueKeys.MetaPaused,
		queue.queueKeys.Paused,
	}

	args := []interface{}{
		queue.queueSettings.MaxStalledCount,
		queue.queueKeys.Empty,
		time.Now().UnixNano() / 1e6,
		queue.queueSettings.StalledInterval,
	}

	return queue.Run("moveStalledJobsToWait", keys, args...)
}

func cleanJobsInSet(queue *Queue, set string, ts int64, limit int) *redis.Cmd {
	var command string
	var removeCommand string
	var breakEarlyCommand = ""
	var hash string
	switch set {
	case "wait", "active", "paused":
		command = "local jobs = redis.call(\"LRANGE\", KEYS[1], 0, -1)"
		removeCommand = "redis.call(\"LREM\", KEYS[1], 0, job)"
		hash = "cleanList"
	case "delayed", "completed", "failed":
		command = "local jobs = redis.call(\"ZRANGE\", KEYS[1], 0, -1)"
		removeCommand = "redis.call(\"ZREM\", KEYS[1], job)"
		hash = "cleanOSet"
	}

	if limit > 0 {
		breakEarlyCommand = strings.Join(
			[]string{"if deletedCount >= limit then", "  break", "end"}, "\n")

		hash = hash + "WithLimit"
	}

	var script = strings.Join([]string{
		command,
		"local deleted = {}",
		"local deletedCount = 0",
		"local limit = tonumber(ARGV[3])",
		"local jobTS",
		"for _, job in ipairs(jobs) do",
		breakEarlyCommand,
		"  local jobKey = ARGV[1] .. job",
		"  if (redis.call(\"EXISTS\", jobKey ..  \":lock\") == 0) then",
		"    jobTS = redis.call(\"HGET\", jobKey, \"timestamp\")",
		"    if(not jobTS or jobTS < ARGV[2]) then",
		removeCommand,
		"      redis.call(\"DEL\", jobKey)",
		"      deletedCount = deletedCount + 1",
		"      table.insert(deleted, job)",
		"    end",
		"  end",
		"end",
		"return deleted",
	}, "\n")
	args := []interface{}{
		queue.queueKeys.Empty,
		ts,
		limit,
	}
	return queue.ExecScript(hash, script, []string{queue.Tokey(set)}, args...)
}

// func retryJobArgs(job Job, ignoreLock bool) ([]string, []interface{}) {
// 	var pushCmd string
// 	queue := job.Queue
// 	jobID := job.ID
// 	keys := []string{
// 		queue.queueKeys.Active,
// 		queue.queueKeys.Wait,
// 		queue.Tokey(jobID),
// 	}
// 	if job.Opts.Lifo {
// 		pushCmd = "R" + "PUSH"
// 	} else {
// 		pushCmd = "L" + "PUSH"

// 	}
// 	args := []interface{}{
// 		pushCmd,
// 		jobID,
// 		queue.token,
// 	}
// 	if ignoreLock {
// 		args[2] = "0"
// 	}
// 	return keys, args
// }

func reprocessJob(job *Job, state string) error {
	queue := job.Queue

	keys := []string{
		queue.Tokey(job.ID),
		queue.Tokey(job.ID) + ":lock",
		queue.Tokey(state),
		queue.Tokey("wait"),
	}
	var args = []interface{}{
		job.ID, "L" + "PUSH", queue.token,
	}
	val, err := queue.Run("reprocessJob", keys, args...).Result()
	if err != nil {
		return err
	}
	if val.(int64) == 0 {
		return errors.New("couldn't retry job: The job doesn't exist")
	}
	if val.(int64) == -1 {
		return errors.New("couldn't retry job: The job is locked")
	}
	if val.(int64) == 0 {
		return errors.New("couldn't retry job: The job has not failed")
	}
	return nil
}
